﻿namespace CodeSHARPer.ViewModel
{
    using Cinch;
    using CodeSHARPer.AvalonEdit.Intellisense;
    using CodeSHARPer.Model;
    using CodeSHARPer.Services;
    using CodeSHARPer.ViewModel.Dialogs;
    using CodeSHARPer.ViewModel.TextBoxControl;
    using CodeSHARPer.ViewModel.Tools;
    using ICSharpCode.AvalonEdit;
    using ICSharpCode.AvalonEdit.Document;
    using ICSharpCode.CodeCompletion;
    using System;
    using System.Globalization;
    using System.IO;

    internal class CodePadViewModel : PaneViewModel, IEditor, IViewAware
    {
        public const string ToolContentId = "FileView";

        private const string defaultCodeProgramScript = "{0}void Main() {0}{{{0}{0}}}{0}{0} // Define other methods and classes here{0}";
        private const string defaultScriptFileType = ".csx";
        private const string defaultFileName = "NewCodeFile";

        private static int newFileCounter = 1;

        /// <summary>
        /// Standard constructor. See also static <seealso cref="LoadFile"/> method
        /// for construction from file saved on disk.
        /// </summary>
        public CodePadViewModel(bool createTempFile, ICSharpCode.CodeCompletion.CodeScriptType codeType)
        {
            Init(createTempFile, codeType);
        }

        private void Init(bool createTempFile, ICSharpCode.CodeCompletion.CodeScriptType codeType)
        {
            this.TxtControl = new TextBoxController();

            if (createTempFile)
            {
                //create a file on the temp folder
                this.FilePath = string.Format(CultureInfo.InvariantCulture, "{0}{1}{2}{3}",
                    Path.GetTempPath(),
                    defaultFileName,
                    CodePadViewModel.newFileCounter++,
                    defaultScriptFileType);

                this.ContentId = this.FilePath;

                //save the default options to the created file
                File.WriteAllText(this.FilePath, Properties.Resources.DefaultOptionsv40 + (codeType == CodeScriptType.Program ? string.Format(defaultCodeProgramScript, Environment.NewLine) : ""));

                this.IsTempFile = true;
            }

            this.IsDirty = false;

            this.document = new TextDocument();

            this.TextEditorSelectionStart = 0;
            this.TextEditorSelectionLength = 0;

            this.SaveCommand = new SimpleCommand<object, object>(CanSave, DoSaveCommand);
            this.CloseCommand = new SimpleCommand<object, object>(DoCloseCommand);
            this.RunCommand = new SimpleCommand<object, object>(DoRunCommand);
            this.OpenContainingFolderCommand = new SimpleCommand<object, object>(DoOpenContainingFolderCommand);
            this.FormatCommand = new SimpleCommand<object, object>(DoFormatCommand);
        }

        internal bool IsTempFile { get; private set; }

        bool isOutputVisible;
        public bool IsOutputVisible
        {
            get { return this.isOutputVisible; }
            set
            {
                this.isOutputVisible = value;
                NotifyPropertyChanged(ObservableHelper.CreateArgs<CodePadViewModel>(x => x.IsOutputVisible));
            }
        }

        #region FilePath
        private string filePath = null;

        /// <summary>
        /// Get/set complete path including file name to where this stored.
        /// This string is never null or empty.
        /// </summary>
        public string FilePath
        {
            get
            {
                if (string.IsNullOrEmpty(this.filePath))
                    return string.Format(CultureInfo.CurrentCulture, "New{1}", defaultScriptFileType);

                return this.filePath;
            }
            private set
            {
                if (this.filePath != value)
                {
                    this.filePath = value;

                    NotifyPropertyChanged(ObservableHelper.CreateArgs<CodePadViewModel>(x => x.FilePath));
                    NotifyPropertyChanged(ObservableHelper.CreateArgs<CodePadViewModel>(x => x.FileName));
                    NotifyPropertyChanged(ObservableHelper.CreateArgs<CodePadViewModel>(x => x.Title));

                    NotifyPropertyChanged(ObservableHelper.CreateArgs<CodePadViewModel>(x => x.CodeCompletion));
                }
            }
        }
        #endregion

        #region Title
        /// <summary>
        /// Title is the string that is usually displayed - with or without dirty mark '*' - in the docking environment
        /// </summary>
        public override string Title
        {
            get
            {
                return this.FileName + (IsDirty ? "*" : string.Empty);
            }
        }
        #endregion

        #region FileName
        /// <summary>
        /// FileName is the string that is displayed whenever the application refers to this file, as in:
        /// string.Format(CultureInfo.CurrentCulture, "Would you like to save the '{0}' file", FileName)
        /// 
        /// Note the absense of the dirty mark '*'. Use the Title property if you want to display the file
        /// name with or without dirty mark when the user has edited content.
        /// </summary>
        public string FileName
        {
            get
            {
                // This option should never happen - its an emergency break for those cases that never occur
                if (FilePath == null || FilePath == String.Empty)
                    return string.Format(CultureInfo.InvariantCulture, "{0}{1}", defaultFileName, defaultScriptFileType);

                return System.IO.Path.GetFileName(FilePath);
            }
        }

        public override Uri IconSource
        {
            get
            {
                // This icon is visible in AvalonDock's Document Navigator window
                return new Uri("Themes/Images/App/Ico/app.newfile.ico", UriKind.RelativeOrAbsolute);
            }
        }
        #endregion FileName

        #region IsReadOnly
        bool isReadOnly = false;
        public bool IsReadOnly
        {
            get { return this.isReadOnly; }
            protected set
            {
                if (this.isReadOnly != value)
                {
                    this.isReadOnly = value;
                    NotifyPropertyChanged(ObservableHelper.CreateArgs<CodePadViewModel>(x => x.IsReadOnly));
                }
            }
        }

        private string isReadOnlyReason = string.Empty;
        public string IsReadOnlyReason
        {
            get { return this.isReadOnlyReason; }
            protected set
            {
                if (this.isReadOnlyReason != value)
                {
                    this.isReadOnlyReason = value;
                    NotifyPropertyChanged(ObservableHelper.CreateArgs<CodePadViewModel>(x => x.IsReadOnlyReason));
                }
            }
        }
        #endregion IsReadOnly

        #region TextContent
        TextDocument document;

        /// <summary>
        /// This property wraps the document class provided by AvalonEdit. The actual text is inside
        /// the document and can be accessed at save, load or other processing times.
        /// 
        /// The Text property itself cannot be bound in AvalonEdit since binding this would result
        /// in updating the text (via binding) each time a user enters a key on the keyboard
        /// (which would be a design error resulting in huge performance problems)
        /// </summary>
        public TextDocument Document
        {
            get { return document; }
            set
            {
                if (document != value)
                {
                    document = value;
                    NotifyPropertyChanged(ObservableHelper.CreateArgs<CodePadViewModel>(x => x.Document));
                }
            }
        }
        #endregion

        #region CodeCompletion
        CSharpCompletion codeCompletion;
        public CSharpCompletion CodeCompletion
        {
            get
            {
                if (codeCompletion == null && !string.IsNullOrEmpty(this.FilePath))
                {
                    ScriptProvider sp = new ScriptProvider(this.FilePath);
                    codeCompletion = new ICSharpCode.CodeCompletion.CSharpCompletion(sp);
                }
                return codeCompletion;
            }
            set
            {
                codeCompletion = value;
                NotifyPropertyChanged(ObservableHelper.CreateArgs<CodePadViewModel>(x => x.CodeCompletion));
            }
        }
        #endregion

        #region IsDirty
        bool isDirty = false;

        /// <summary>
        /// IsDirty indicates whether the file currently loaded in the editor was modified by the user or not.
        /// </summary>
        public bool IsDirty
        {
            get { return isDirty; }
            set
            {
                if (isDirty != value)
                {
                    isDirty = value;

                    NotifyPropertyChanged(ObservableHelper.CreateArgs<CodePadViewModel>(x => x.IsDirty));
                    NotifyPropertyChanged(ObservableHelper.CreateArgs<CodePadViewModel>(x => x.Title));
                }
            }
        }
        #endregion

        #region AvalonEdit properties

        private bool showLineNumbers = Workspace.Instance.Config.CodeEditorOptions.ShowLineNumbers;
        public bool ShowLineNumbers
        {
            get { return this.showLineNumbers; }
            set
            {
                if (this.showLineNumbers != value)
                {
                    this.showLineNumbers = value;
                    NotifyPropertyChanged(ObservableHelper.CreateArgs<CodePadViewModel>(x => x.ShowLineNumbers));
                }
            }
        }

        string font = Workspace.Instance.Config.CodeEditorOptions.EditorFont;
        public string Font
        {
            get { return font; }
            set
            {
                font = value;
                NotifyPropertyChanged(ObservableHelper.CreateArgs<CodePadViewModel>(c => c.Font));
            }
        }

        int fontSize = Workspace.Instance.Config.CodeEditorOptions.FontSize;
        public int FontSize
        {
            get { return fontSize; }
            set
            {
                fontSize = value;
                NotifyPropertyChanged(ObservableHelper.CreateArgs<CodePadViewModel>(c => c.FontSize));
            }
        }

        TextEditorOptions textEditorOptions = Workspace.Instance.Config.CodeEditorOptions.Options;
        public TextEditorOptions TextEditorOptions
        {
            get { return textEditorOptions; }
            set
            {
                textEditorOptions = value;
                NotifyPropertyChanged(ObservableHelper.GetPropertyName<CodePadViewModel>(c => c.TextEditorOptions));
            }
        }

        #endregion AvalonEdit properties

        #region LoadFile
        public static CodePadViewModel LoadFile(string filePath)
        {
            if (File.Exists(filePath) == false || Path.GetExtension(filePath).Equals(defaultScriptFileType, StringComparison.OrdinalIgnoreCase) == false)
                return null;

            CodePadViewModel vm = new CodePadViewModel(createTempFile: false, codeType: ICSharpCode.CodeCompletion.CodeScriptType.Snippet);
            vm.IsTempFile = false;

            if (vm.OpenFile(filePath) == true)
                return vm;

            return null;
        }

        /// <summary>
        /// Attempt to open a file and load it into the viewmodel if it exists.
        /// </summary>
        /// <param name="filePath"></param>
        /// <returns>True if file exists and was succesfully loaded. Otherwise false.</returns>
        protected bool OpenFile(string filePath)
        {
            try
            {
                if (File.Exists(filePath))
                {
                    this.FilePath = filePath;
                    this.ContentId = filePath;
                    IsDirty = false;

                    // Check file attributes and set to read-only if file attributes indicate that
                    if ((System.IO.File.GetAttributes(filePath) & FileAttributes.ReadOnly) != 0)
                    {
                        this.IsReadOnly = true;
                        this.IsReadOnlyReason = "This file cannot be edit because you do not have write access.\n" +
                                                "Change the file access permissions or save the file in a different location if you want to edit it.";
                    }

                    try
                    {
                        this.document = new TextDocument();
                    }
                    catch (Exception ex)
                    {
                        // File may be blocked by another process
                        ServiceResolver.Instance.MessageBoxService.ShowError(ex.Message, "An error has occurred");

                        return false;
                    }
                }
                else
                    return false;
            }
            catch (Exception exp)
            {
                ServiceResolver.Instance.MessageBoxService.ShowError(exp.Message, "An error has occurred");

                return false;
            }

            return true;
        }
        #endregion LoadFile

        #region SaveCommand
        public SimpleCommand<object, object> SaveCommand { get; set; }

        /// <summary>
        /// Save the document viewed in this viewmodel.
        /// </summary>
        public bool CanSave(object args)
        {
            return this.IsDirty;
        }

        public void DoSaveCommand(object args)
        {
            SaveFile();
        }

        /// <summary>
        /// Write text content to disk and (re-)set associated properties
        /// </summary>
        /// <param name="filePath"></param>
        internal void SaveFile(string filePath = "")
        {
            if (!string.IsNullOrEmpty(filePath))
                FilePath = filePath;

            if (this.CodeCompletion != null)
            {
                //save the document with the options
                this.CodeCompletion.ScriptProvider.Save(FilePath, this.Document.Text);
                this.IsDirty = false;
            }
        }
        #endregion

        #region SaveAsCommand
        public bool CanSaveAs()
        {
            return true;
        }

        public bool DoSaveAs()
        {
            return (Workspace.Instance.OnSave(this, true));
        }
        #endregion

        #region CloseCommand
        /// <summary>
        /// This command closes a single file. The binding for this is in the AvalonDock LayoutPanel Style.
        /// </summary>
        public SimpleCommand<object, object> CloseCommand { get; private set; }

        internal void DoCloseCommand(object args)
        {
            Workspace.Instance.Close(this);
        }
        #endregion

        #region OpenContainingFolderCommand
        public SimpleCommand<object, object> OpenContainingFolderCommand { get; private set; }

        internal void DoOpenContainingFolderCommand(object args)
        {
            try
            {
                if (System.IO.File.Exists(this.FilePath) == true)
                {
                    // combine the arguments together it doesn't matter if there is a space after ','
                    string argument = @"/select, " + this.FilePath;

                    System.Diagnostics.Process.Start("explorer.exe", argument);
                }
                else
                {
                    string parentDir = System.IO.Directory.GetParent(this.FilePath).FullName;

                    if (System.IO.Directory.Exists(parentDir) == false)
                        ServiceResolver.Instance.MessageBoxService.ShowError(
                            string.Format(CultureInfo.CurrentCulture, "The directory '{0}' does not exist or cannot be accessed.", parentDir),
                            "Error finding file");
                    else
                    {
                        string argument = @"/select, " + parentDir;

                        System.Diagnostics.Process.Start("EXPLORER.EXE", argument);
                    }
                }
            }
            catch (System.Exception ex)
            {
                ServiceResolver.Instance.MessageBoxService.ShowError(
                    string.Format(CultureInfo.CurrentCulture, "{0}\n'{1}'.", ex.Message, (this.FilePath ?? string.Empty)),
                    "Error finding file:");
            }
        }
        #endregion OpenContainingFolder

        #region CopyFullPathtoClipboardCommand
        public SimpleCommand<object, object> CopyFullPathtoClipboardCommand { get; private set; }

        internal void DoCopyFullPathtoClipboardCommand(object args)
        {
            try
            {
                System.Windows.Clipboard.SetText(this.FilePath);
            }
            catch { }
        }
        #endregion CopyFullPathtoClipboard

        #region RunCommand
        public SimpleCommand<object, object> RunCommand { get; private set; }

        internal void DoRunCommand(object args)
        {
            (this.View as CodeSHARPer.View.ICodePadView).RunCode();
            this.IsOutputVisible = true;
        }
        #endregion

        #region Format
        public SimpleCommand<object, object> FormatCommand { get; private set; }

        internal void DoFormatCommand(object args)
        {
            ICSharpCode.NRefactory.CSharp.CSharpFormatter formater =
                new ICSharpCode.NRefactory.CSharp.CSharpFormatter(ICSharpCode.NRefactory.CSharp.FormattingOptionsFactory.CreateAllman());
            //formater.AddFormattingRegion(new ICSharpCode.NRefactory.TypeSystem.DomRegion(this.FilePath, 2, 0, this.Document.LineCount, this.Document.Lines[this.Document.LineCount - 1].EndOffset));

            string formatedCode = null;
            if ((args as bool?).GetValueOrDefault())
                formatedCode = formater.Format(this.CodeCompletion.ScriptProvider.GenerateCodeForFormating(this.Document.Text));
            else
            {
                string selectedText = null;
                this.TxtControl.GetSelectedText(out selectedText);
                this.Document.Text = formater.Format(selectedText);
            }

            this.Document.BeginUpdate();
            this.Document.Text = this.CodeCompletion.ScriptProvider.ReadFromFormatedCode(formatedCode);
            this.Document.EndUpdate();
        }
        #endregion

        #region CaretPosition
        // These properties are used to display the current column/line
        // of the cursor in the user interface
        private int mLine = 0;
        public int Line
        {
            get { return this.mLine; }
            set
            {
                if (this.mLine != value)
                {
                    this.mLine = value;
                    NotifyPropertyChanged(ObservableHelper.CreateArgs<CodePadViewModel>(x => x.Line));
                }
            }
        }

        private int mColumn = 0;
        public int Column
        {
            get { return this.mColumn; }
            set
            {
                if (this.mColumn != value)
                {
                    this.mColumn = value;
                    NotifyPropertyChanged(ObservableHelper.CreateArgs<CodePadViewModel>(x => x.Column));
                }
            }
        }
        #endregion CaretPosition

        #region EditorStateProperties
        // These properties are used to save and restore the editor state when CTRL+TABing between documents
        private int mTextEditorCaretOffset = 0;
        private int mTextEditorSelectionStart = 0;
        private int mTextEditorSelectionLength = 0;
        private bool mTextEditorIsRectangularSelection = false;
        private double mTextEditorScrollOffsetX = 0;
        private double mTextEditorScrollOffsetY = 0;

        /// <summary>
        /// Get/set editor carret position
        /// for CTRL-TAB Support http://avalondock.codeplex.com/workitem/15079
        /// </summary>
        public int TextEditorCaretOffset
        {
            get { return this.mTextEditorCaretOffset; }
            set
            {
                if (this.mTextEditorCaretOffset != value)
                {
                    this.mTextEditorCaretOffset = value;
                    NotifyPropertyChanged(ObservableHelper.CreateArgs<CodePadViewModel>(x => x.TextEditorCaretOffset));
                }
            }
        }

        /// <summary>
        /// Get/set editor start of selection
        /// for CTRL-TAB Support http://avalondock.codeplex.com/workitem/15079
        /// </summary>
        public int TextEditorSelectionStart
        {
            get { return this.mTextEditorSelectionStart; }
            set
            {
                if (this.mTextEditorSelectionStart != value)
                {
                    this.mTextEditorSelectionStart = value;
                    NotifyPropertyChanged(ObservableHelper.CreateArgs<CodePadViewModel>(x => x.TextEditorSelectionStart));
                }
            }
        }

        /// <summary>
        /// Get/set editor length of selection
        /// for CTRL-TAB Support http://avalondock.codeplex.com/workitem/15079
        /// </summary>
        public int TextEditorSelectionLength
        {
            get { return this.mTextEditorSelectionLength; }
            set
            {
                if (this.mTextEditorSelectionLength != value)
                {
                    this.mTextEditorSelectionLength = value;
                    NotifyPropertyChanged(ObservableHelper.CreateArgs<CodePadViewModel>(x => x.TextEditorSelectionLength));
                }
            }
        }

        public bool TextEditorIsRectangularSelection
        {
            get { return this.mTextEditorIsRectangularSelection; }
            set
            {
                if (this.mTextEditorIsRectangularSelection != value)
                {
                    this.mTextEditorIsRectangularSelection = value;
                    NotifyPropertyChanged(ObservableHelper.CreateArgs<CodePadViewModel>(x => x.TextEditorIsRectangularSelection));
                }
            }
        }

        #region EditorScrollOffsetXY
        /// <summary>
        /// Current editor view scroll X position
        /// </summary>
        public double TextEditorScrollOffsetX
        {
            get { return this.mTextEditorScrollOffsetX; }
            set
            {
                if (this.mTextEditorScrollOffsetX != value)
                {
                    this.mTextEditorScrollOffsetX = value;
                    NotifyPropertyChanged(ObservableHelper.CreateArgs<CodePadViewModel>(x => x.TextEditorScrollOffsetX));
                }
            }
        }

        /// <summary>
        /// Current editor view scroll Y position
        /// </summary>
        public double TextEditorScrollOffsetY
        {
            get { return this.mTextEditorScrollOffsetY; }
            set
            {
                if (this.mTextEditorScrollOffsetY != value)
                {
                    this.mTextEditorScrollOffsetY = value;
                    NotifyPropertyChanged(ObservableHelper.CreateArgs<CodePadViewModel>(x => x.TextEditorScrollOffsetY));
                }
            }
        }
        #endregion EditorScrollOffsetXY
        #endregion EditorStateProperties

        private TextBoxController mTxtControl = null;
        public TextBoxController TxtControl
        {
            get { return this.mTxtControl; }
            private set
            {
                if (this.mTxtControl != value)
                {
                    this.mTxtControl = value;
                    NotifyPropertyChanged(ObservableHelper.CreateArgs<CodePadViewModel>(x => x.TxtControl));
                }
            }
        }

        #region IEditorInterface
        public string Text
        {
            get
            {
                return this.Document == null ? string.Empty : this.Document.Text;
            }
        }

        public int SelectionStart
        {
            get
            {
                int start = 0, length = 0;
                bool IsRectSelect = false;

                if (this.TxtControl != null)
                    this.TxtControl.CurrentSelection(out start, out length, out IsRectSelect);

                return start;
            }
        }

        public int SelectionLength
        {
            get
            {
                int start = 0, length = 0;
                bool IsRectSelect = false;

                if (this.TxtControl != null)
                    this.TxtControl.CurrentSelection(out start, out length, out IsRectSelect);

                return length;
            }
        }

        /// <summary>
        /// Selects the specified portion of Text and scrolls that part into view.
        /// </summary>
        /// <param name="start"></param>
        /// <param name="length"></param>
        public void Select(int start, int length)
        {
            if (this.TxtControl != null)
                this.TxtControl.SelectText(start, length);
        }

        public void Replace(int start, int length, string ReplaceWith)
        {
            if (this.Document != null)
                this.Document.Replace(start, length, ReplaceWith);
        }

        /// <summary>
        /// This method is called before a replace all operation.
        /// </summary>
        public void BeginChange()
        {
            if (this.TxtControl != null)
                this.TxtControl.BeginChange();
        }

        /// <summary>
        /// This method is called after a replace all operation.
        /// </summary>
        public void EndChange()
        {
            if (this.TxtControl != null)
                this.TxtControl.EndChange();
        }
        #endregion IEditorInterface

        /// <summary>
        /// Get the path of the file or empty string if file does not exists on disk.
        /// </summary>
        /// <returns></returns>
        internal string GetFilePath()
        {
            try
            {
                if (System.IO.File.Exists(this.FilePath))
                    return System.IO.Path.GetDirectoryName(this.FilePath);
            }
            catch { }

            return string.Empty;
        }

        internal void UpdateCodeEditorProperties()
        {
            this.ShowLineNumbers = Workspace.Instance.Config.CodeEditorOptions.ShowLineNumbers;
            this.Font = Workspace.Instance.Config.CodeEditorOptions.EditorFont;
            this.FontSize = Workspace.Instance.Config.CodeEditorOptions.FontSize;
            this.TextEditorOptions = Workspace.Instance.Config.CodeEditorOptions.Options;
        }

        /// <summary>
        /// Reference to the View, cast to ICodePadView
        /// </summary>
        public object View { get; set; }
    }
}
